package com.gitee.wsl.net.ssdp

import com.gitee.wsl.logger.Logger
import com.gitee.wsl.logger.debug
import com.gitee.wsl.logger.error
import com.gitee.wsl.net.ssdp.message.Advertisement
import com.gitee.wsl.net.ssdp.message.Headers
import com.gitee.wsl.net.ssdp.message.SsdpMessage
import com.gitee.wsl.net.ssdp.message.SsdpMessageType
import com.gitee.wsl.net.ssdp.message.Update
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.consumeAsFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import java.net.InetAddress
import java.net.NetworkInterface
import kotlin.coroutines.CoroutineContext

class NotificationService(
    cpName:String,
    interfaces: List<NetworkInterface>,
    val coroutineContext: CoroutineContext = Dispatchers.IO + SupervisorJob(),
    val pool: INotifyServerPool = NotifyServerPool(cpName, interfaces, coroutineContext)
): INotificationService {

    private val activeNodes = mutableMapOf<String, SsdpMessage>()

    override val addNotifications: Flow<AddNotification> = pool.messages
        .consumeAsFlow()
        .filter { it.type == SsdpMessageType.NotifyAlive || it.type == SsdpMessageType.SearchResponse }
        .map(::processAdvertisement)
        .filterNotNull()

    override val removeNotifications: Flow<String> = pool.messages
        .consumeAsFlow()
        .filter { it.type == SsdpMessageType.NotifyUpdate || it.type == SsdpMessageType.NotifyByeBye }
        .map(::processUpdate)
        .filterNotNull()

    private fun processAdvertisement(msg: SsdpMessage): AddNotification? {
        val ad = Advertisement.fromSsdpMessage(msg)
        Logger.debug("New advertisement: $ad")
        if(ad.searchPort != null){
            // TODO Implement custom search ports
            Logger.error("Custom search ports are not implemented! Ignoring search response: $ad")
            return null
        }
        if(!ad.isValid()){
            Logger.debug("Advertisement expired: $ad")
            return null
        }
        activeNodes[ad.usn] = msg
        return AddNotification(ad.usn, ad.location, ad.server, ad.ttl)
    }

    private fun processUpdate(msg: SsdpMessage): String?{
        val usn : String? = if(msg.type == SsdpMessageType.NotifyByeBye){
            processByeByeAndGetUsn(msg)
        } else {
            processUpdateAndGetUsn(msg)
        }
        return usn?.also { activeNodes.remove(it) }
    }

    private  fun processUpdateAndGetUsn(msg: SsdpMessage):String? {
        val upd = Update.fromSsdpMessage(msg)
        Logger.debug("New update: $upd")
        activeNodes[upd.usn]?.let {node ->
            val bootId = node.getRequiredHeader(Headers.BootId).toInt()
            if(bootId == upd.bootId){
                node.setHeader(Headers.BootId, upd.nextBootId.toString())
                return null
            } else {
                return upd.usn
            }
        }
        // Update's bootId is not equal to the advertised earlier. Request node info anew
        Logger.debug("Update for unknown device or with unexpected BootID: {}", upd.usn)
        // take the UUID part of the USN and search for it
        val searchTarget = upd.usn.split("::")[0]
        pool.search(searchTarget, upd.inetAddr)
        return null
    }

    private fun processByeByeAndGetUsn(msg: SsdpMessage): String {
        val usn = msg.getRequiredHeader(Headers.UniqueServiceName)
        Logger.debug("ByeBye message from: {}", usn)
        return usn
    }

    override fun start() {
        Logger.debug("Starting NotificationService")
        pool.start()
    }

    override fun search(target: String?, inetAddr: InetAddress?) { pool.search(target, inetAddr) }

    override fun stop() {
        pool.stop()
        coroutineContext.cancel()
    }
}